除了我們在平時作業上時常使用 Azure DevOps Boards 來查看專案進度情形,另一個相關的重要功能就屬於 Outlook 上的郵件信箱與行事曆工具了。在公司內部預設配給每位員工一個 Outlook 組織的信箱,除了內部的 ERP 留言功能許多通知也會寄送到 Outlook mail 中 ; 另外是小組內使用的 Outlook 行事曆,我們會將重要的會議或是事件以及小組成員的請假時間都放上去,並且視職位不同有些人可能會需要同時多個行事曆需求。這兩者都是每天一定會使用到的功能,並且 Outlook 的相關後端服務也是部署於內部地端,而非使用雲端,如此來看按照上一篇的 Boards,Outlook 也會有相關的 API 能夠調用開發,這就是我們本篇的主題 Exchange Web Services (EWS) API。
為了能夠在 Backstage 上同步顯示資訊 ,我們採用了 EWS API 提供的功能。EWS 允許我們透過 API 直接存取 Outlook 使用者的電子郵件、日曆和聯絡人資訊,讓 Backstage 能夠取得資訊再進行前端資訊的呈現設計,讓使用者能夠直接在個人首頁中查看郵件和日曆更新,除了使用 Boards 的查看代辦事項,也能順帶把事件(郵件)、時間線(行事曆) 的資訊一次輕鬆取得,相信更能幫助使用者整理思緒。
上方第一個插件為 Outlook 日曆、下方為 Outlook 信件。
可以點擊前往 Outlook 日曆,將開啟新分頁轉跳到行事曆總表,方便更查看更細節內容。
點擊 Outlook 郵件一樣可以開啟新分頁轉跳到該信件內容。
在這兩個功能的部分,我們一樣以 Backstage 提供捷徑給使用者的宗旨,透過 Backstage 連結重要資訊並便於查看,進一步修改等行為再另外進入該應用中操作,所以要實現這項功能非常單純,我們只需想辦法透過 API 讀取所需資料。
Exchange Web Services (EWS) API 是一個強大的工具,專門用來讓應用程式與 Exchange Server 進行互動。EWS API 提供了多樣化的功能,包括收發郵件、日曆事件、管理聯絡人等,適合需要整了郵件或日曆管理功能的應用場景。
整合 Exchange Server 的方法不只一種,面對我們架設在地端的方式,使用 EWS API 來調用資料就足夠,若是使用雲端版現已有更新的方式,則不太建議使用 EWS。要開始串接前我們要先找出 EWS API 的調用端口,例如以下圖片就是我們內部 Exchange 的 API 端口,接著我們就會針對這個 URL 進行 API 的請求,取得使用者的信件與行事曆內容。
與 EWS API 進行交互時,我們必須驗證使用者的身份與授權,EWS 支援兩種認證方式,包括:
不過在這邊我們必須選擇基本認證的方法,因為公司在其他系統與 AD 身份有關的時,也採用了帳號密碼登入的認證方式,而沒有支援使用主流的 OAuth2.0,也許是因為我們只會在內部網路使用或是其他原因,不過採用基本認證讓我們在開發上的難度減輕許多。
因為涉及到需要使用身份驗證後,再帶入資料對 Exchange API 呼叫使用,過程中需要使用者輸入帳號密碼,並且會數次調用該 API 其資料量可多可少,面對這種較為複雜的應用需求,我們採用前後端插件來分別開發功能。
抱著實驗性質與讓使用者不必每次重複輸入帳號密碼等,在後端開發時加入了以下機制,使用 Cookie、AES 加密、以及 Redis 來管理和存儲用戶的帳號密碼與日曆 ID 的保存:
Cookie (儲存 Session ID):
將使用者的 email 儲存在 Cookie 中,以便後續請求時可以自動取得該使用者資料進行驗證。
AES (加密帳密明文):
在將使用者的敏感資訊存儲到 Redis 前,使用 AES 對稱加密算法來加密,確保敏感資料不會以明文形式暴露。
Redis (快速緩存):
Redis 用來做快速緩存以便存取,將加密過的資訊(帳號密碼等)儲存在其中,讓系統可以自動取出進行驗證。
如此一來在實現功能後,使用者只需要輸入一次帳號密碼,之後就會藉由儲存在 Cookie 中的 mail,自動到 Redis 取得帳號密碼帶入驗證,為了確保安全性中間加上了 AES 加密機制。
基本驗證方法下衍生出的優化架構,我們只需在首次輸入帳號密碼
以下代碼僅供參考,當初在開發時追求快速成品來實驗,故沒有進行程式的重構與整理,其中包含大量重複的程式碼,讀者只需理解關鍵觀念即可。
我們使用現成的套件來幫助我們快速調用 ews-javascript-api ,依靠這個套件我們可以快速實現大多數需要的功能。
介面定義
Redis、Cookie、AES 實作的定義設定
驗證檢查 Middleware
Express.js 的 middleware,負責驗證檢查使用者的登入狀態,並從 Redis 中取得加密存儲的帳號憑證,然後解密並傳遞給後續的請求處理。
在每個請求進行時:
userEmail
)。req.userCredentials
),供後續處理使用。如果使用者未登入或 Redis 中不存在憑證,則會返回 401 錯誤,拒絕未授權的請求。
當使用者輸入帳號密碼進行登入時,會先呼叫此方法儲存相關資訊,並且透過 API 取得使用者本人的日曆列表 ID 存入 Redis 當中。
首次登入時就會抓取到個人行事曆的列表
authMiddleware
,從 Redis 中取出使用者加密的憑證,解密後獲取 email 和 password。ExchangeService
和用戶的憑證 (email
和 password
) 連接到 EWS (Exchange Web Services)。view.PropertySet
),只查詢收件箱 (WellKnownFolderName.Inbox
) 中的郵件項目。可選擇返回最多 top
條郵件 (預設為 100)。EmailMessage
,並將其轉換為一個包含 subject
、receivedDate
、from
和 link
等欄位的簡化物件格式。在處理 OWA 連結時,我們遇到使用者連結到該信件內容時會出現錯誤,然而並不是每個人、每封信都會出現類似的問題,我們甚至聯絡了微軟支援也沒找出什麼頭緒。最後,我們發現問題的根源在於:
EWS API 使用 Base64 編碼來表示信件的資料夾路徑,而 Base64 的結果
ItemId.UniqueId
可能包含+
符號,當這些字符出現在 URL 中時,可能被誤解為空格,導致 URL 解析失敗。為了解決此問題,我們目前的處理方法是將
+
替換為%2B
,即使用.replace(/\+/g, '%2B')
,這樣可以確保 URL 正常解析和讀取。
根據使用者的特定日曆ID和時間範圍,取得該日曆中的相關事件,並將這些事件以JSON 格式傳回客戶端,在我們查看特定行事曆並按下前後天時就會進行一次查詢,取出當天的事件。例如在查看 9/12、11 或是 13 日或切換行事曆時,就會攜帶這個行事曆的 ID、時間範圍 (預設為1天),並依據設定要查詢的欄位返回資料,最後將它們轉換為一個簡單的結構化格式。每個事件都包含唯一的 ID、主題、開始時間、結束時間、地點等資訊,並以陣列形式保存回傳到前端顯示。
對於要讀取到其他人共享給我們的行事曆,目前還沒找出方法從 API 中單獨抓出,我們僅能取得他人分享的行事曆 ID,再使用插件的加入行事曆功能來一起顯示。在加入之前請先確定在 Outlook 行事曆的頁面上已經可以看到別人共享給你的行事曆,分享 ID 的方法才會生效。
相關功能並不複雜,礙於篇幅原因將一並放到最後的參考程式中一起提供。
前端的部分可以將兩個不同功能做成不同的頁面,這樣做的可以將它們都整合在同一個前端插件中,架構如下,而前端的程式碼我將會放在最後讓各位自行參考。
至於更詳細的程式碼可以參考我放在 Github 中的範例,以上本文只提到重點的開發觀念,幾本上擁有觀念後就能夠自行開發需要的用途。若想更進一步瞭解完整的前後端專案的寫法,請再自行下載研究。
https://github.com/Jincoco88912/backstage-plugin-outlook-ews
https://www.npmjs.com/package/ews-javascript-api
https://blog.darkthread.net/blog/ews-office365/